﻿using System;
using System.Buffers;
using System.Globalization;

namespace Blazorise.Utilities;

/// <summary>
/// Copy from https://github.com/dotnet/aspnetcore/blob/ee4a0e9801c4e1e673d0373e1c51a6790de174cc/src/Components/Shared/src/ExpressionFormatting/ReverseStringBuilder.cs#L22
/// </summary>
internal ref struct ReverseStringBuilder
{
    public const int MinimumRentedArraySize = 1024;

    private static readonly ArrayPool<char> s_arrayPool = ArrayPool<char>.Shared;

    private int _nextEndIndex;
    private Span<char> _currentBuffer;
    private SequenceSegment _fallbackSequenceSegment = null;

    // For testing.
    internal int SequenceSegmentCount => _fallbackSequenceSegment?.Count() ?? 0;

    public ReverseStringBuilder( int conservativeEstimatedStringLength )
    {
        var array = s_arrayPool.Rent( conservativeEstimatedStringLength );
        _fallbackSequenceSegment = new( array );
        _currentBuffer = array;
        _nextEndIndex = _currentBuffer.Length;
    }

    public ReverseStringBuilder( Span<char> initialBuffer )
    {
        _currentBuffer = initialBuffer;
        _nextEndIndex = _currentBuffer.Length;
    }

    public bool Empty => _nextEndIndex == _currentBuffer.Length;

    public void InsertFront( scoped ReadOnlySpan<char> span )
    {
        var startIndex = _nextEndIndex - span.Length;
        if ( startIndex >= 0 )
        {
            // The common case. There is enough space in the current buffer to copy the given span.
            // No additional work needs to be done here after the copy.
            span.CopyTo( _currentBuffer[startIndex..] );
            _nextEndIndex = startIndex;
            return;
        }

        // There wasn't enough space in the current buffer.
        // What we do next depends on whether we're writing to the provided "initial" buffer or a rented one.

        if ( _fallbackSequenceSegment is null )
        {
            // We've been writing to a stack-allocated buffer, but there is no more room on the stack.
            // We rent new memory with a length sufficiently larger than the initial buffer
            // and copy the contents over.
            var remainingLength = -startIndex;
            var sizeToRent = _currentBuffer.Length + Math.Max( MinimumRentedArraySize, remainingLength * 2 );
            var newBuffer = s_arrayPool.Rent( sizeToRent );
            _fallbackSequenceSegment = new( newBuffer );

            _nextEndIndex = newBuffer.Length - _currentBuffer.Length;
            _currentBuffer.CopyTo( newBuffer.AsSpan()[_nextEndIndex..] );
            _currentBuffer = newBuffer;

            startIndex = _nextEndIndex - span.Length;
            span.CopyTo( _currentBuffer[startIndex..] );
            _nextEndIndex = startIndex;
        }
        else
        {
            // We can't fit the whole string in the current heap-allocated buffer.
            // Copy as much as we can to the current buffer, rent a new buffer, and
            // continue copying the remaining contents.
            var remainingLength = -startIndex;
            span[remainingLength..].CopyTo( _currentBuffer );
            span = span[..remainingLength];

            var sizeToRent = Math.Max( MinimumRentedArraySize, remainingLength * 2 );
            var newBuffer = s_arrayPool.Rent( sizeToRent );
            _fallbackSequenceSegment = new( newBuffer, _fallbackSequenceSegment );
            _currentBuffer = newBuffer;

            startIndex = _currentBuffer.Length - remainingLength;
            span.CopyTo( _currentBuffer[startIndex..] );
            _nextEndIndex = startIndex;
        }
    }

    public void InsertFront<T>( T value ) where T : ISpanFormattable
    {
        // This is large enough for any integer value (10 digits plus the possible sign).
        // We won't try to optimize for anything larger.
        Span<char> result = stackalloc char[11];

        if ( value.TryFormat( result, out var charsWritten, format: default, CultureInfo.InvariantCulture ) )
        {
            InsertFront( result[..charsWritten] );
        }
        else
        {
            InsertFront( (IFormattable)value );
        }
    }

    public void InsertFront( IFormattable formattable )
        => InsertFront( formattable.ToString( null, CultureInfo.InvariantCulture ) );

    public override readonly string ToString()
        => _fallbackSequenceSegment is null
            ? new( _currentBuffer[_nextEndIndex..] )
            : _fallbackSequenceSegment.ToString( _nextEndIndex );

    public readonly void Dispose()
    {
        _fallbackSequenceSegment?.Dispose();
    }

    private sealed class SequenceSegment : ReadOnlySequenceSegment<char>, IDisposable
    {
        private readonly char[] _array;

        public SequenceSegment( char[] array, SequenceSegment? next = null )
        {
            _array = array;
            Memory = array;
            Next = next;
        }

        // For testing.
        internal int Count()
        {
            var count = 0;
            for ( var current = this; current is not null; current = current.Next as SequenceSegment )
            {
                count++;
            }
            return count;
        }

        public string ToString( int startIndex )
        {
            RunningIndex = 0;

            var tail = this;
            while ( tail.Next is SequenceSegment next )
            {
                next.RunningIndex = tail.RunningIndex + tail.Memory.Length;
                tail = next;
            }

            var sequence = new ReadOnlySequence<char>( this, startIndex, tail, tail.Memory.Length );
            return sequence.ToString();
        }

        public void Dispose()
        {
            for ( var current = this; current is not null; current = current.Next as SequenceSegment )
            {
                s_arrayPool.Return( current._array );
            }
        }
    }
}